Skip to content

refactor(ui): co-locate component styles and split Deck and Panel out of the full player - #472

Merged
chhoumann merged 10 commits into
masterfrom
refactor/ui-structure
Oct 14, 2026
Merged

chhoumann merged 10 commits into
masterfrom
refactor/ui-structure

Conversation

@chhoumann

@chhoumann chhoumann commented Oct 14, 2026 •

Copy link
Copy Markdown
Owner

Why

The v3 view grew in three places that are hard to read. PodcastView.svelte carried about 1,300 lines of page and component CSS in one global block. Player.svelte was 916 lines. The verify skill's screens.mjs passed 1,100 lines. This PR restructures them so each component owns its styles. Every screen stays pixel-identical, apart from three small fixes and one keyboard addition.

What changed

  • Each component co-locates its styles, anchored on its own root class with the .root :global {} pattern the shell already used. The shell keeps its tokens, its layout, the rules Obsidian's DOM needs, and the classes several components share (PodcastView.svelte 1,552 → 739 lines).
  • Player.svelte (916 → 530 lines) hands the title, scrubber, transport and capture bar to Deck.svelte. ChaptersPanel and UpNextPanel share their chrome through Panel.svelte. The headset's Next in queue moves from bindMediaSession into PlayerController.playNext.
  • screens.mjs (1,128 → 189 lines) splits into screens/instance.mjs, screens/scenes.mjs and screens/sheet.mjs, verbatim apart from imports and paths. The ignored FeedRepository cache option and its 8 frozen test callers are gone.

Behavior changes:

  • The wide player's Chapters and Up next tabs follow the ARIA tabs pattern: Left and Right move and wrap, Home and End jump, and only the selected tab is in the Tab order.
  • The fullscreen button hides while its video frame is fullscreen, where it did nothing. Escape leaves fullscreen and, as before, also collapses the full player.
  • The play button presses without its 120 ms transition when the system asks for reduced motion.
  • After an import whose final save fails, the Notice says PodNotes saves the settings once it can, instead of asking the user to change a setting (fix(persistence): recover transient save refusals without edits #470 made recovery automatic).

Scope

  • Covers the structure and the four changes above. Phone, accessibility and copy polish is UI4b's.
  • Leaves docs/scripts/screenshots.mjs alone. It drives the v2 UI, and the release docs pass rewrites it.

Where a component styles a child's markup

Each case is one the parent's layout decides:

  • EpisodeRow, AddPodcastPage (results), LibraryPage (grid cards) and EpisodesPage (hero) size the Cover they place.
  • Panel centers the empty state its list shows.
  • Player styles the <video> element that player.videoStage inserts.
  • The shell places SearchField in its head and lets a page's nav list bleed into the page padding.

Queue rows are an EpisodeRow variant (queue, dragging) instead of rules in QueueList.

Tradeoffs

  • Svelte's per-element scoping was tried first. It cost 1,255 gzip bytes for no visible change, so the coordinator chose root-anchored blocks.
  • A rule that varies a shared class, such as .pn-iconbtn.pn-grip, now carries a button type selector. It wins by specificity instead of stylesheet order, and still loses to the shared :hover.
  • Each button keeps its master specificity against Obsidian's touch rule button:not(.clickable-icon).mobile-tap. The mini player's open button and the rail's add-podcast button stay plain scoped rules, so they still show Obsidian's tap fill as before. svelte:element would have merged Panel's two branches but costs 1.9K gzip.

Blast radius

The whole PodNotes view restyles through new selectors. A cascade mistake would show as a wrong size, color or hover state. The pixel and computed-style comparisons below cover every captured state, at rest, under hover, active and focus, and under Obsidian's touch class. main.js gzip goes from 98,345 to 98,906 bytes (+561, under the 100,000 gate).

Step gzip
master d502219 98,345
Cache option removed (on 400e372) 98,075 (±0)
playNext 98,077 (+2)
Import Notice, merge of master 98,340
Styles co-located, Deck and Panel 98,672 (+332)
Tab arrow keys 98,761 (+89)
Fullscreen and reduced-motion fixes 98,781 (+20)
Review round: master specificity on touch, queue row variant 98,906 (+125)
For reference: per-element scoping instead 99,595

Verification

  • Pixel diff, master vs head, 136 shots (23 states × sidebar 320, pane 820, phone 390 × light, dark): 133 identical. The 3 that differ are capture noise. One is a video frame (12 px), which also differs between two master runs. The others are the two phone Up next states (40 and 42 px), where the deck sits at a fractional offset that is the same on both builds (398.40625 px); master recaptured in the same session differs from its own baseline there by 49 and 51 px. The split screens.mjs captures the same shots as the old one on the same bundle.
  • Computed styles of every element in the view, at rest, with :hover, :active and :focus-visible forced, and with Obsidian's touch class mobile-tap on every tappable element, master vs head: all 68 state-widths identical. The search results' new wrapper is folded out of the element paths for that comparison. In the now-playing sidebar state, one master snapshot caught master's capture buttons at the shared .pn-btn height (30 px instead of 36 px): master's .pn-player .pn-cap ties .pn-btn and wins only by stylesheet order, and button.pn-cap no longer depends on it. A settled master snapshot of that state matches.
  • Real Obsidian 1.14.4 with real key events, master then head: the tabs ignore arrow keys on master and move on head; under reduced motion the play transition is transform 0.12s on master and none on head; in fullscreen the button shows (32 × 32) on master and is hidden on head.
  • npm run lint, format:check, typecheck, build, test (2,549 passed), test:tooling (13 passed), docs:build, and test:e2e on a fresh vault (176 of 176 passed), all on Node 22.23.2.

Mutations

Each wrong alternative below fails the named test:

Mutation Failing test
playNext plays the queue's first entry, even a copy of the current episode PlayerController.test.ts playNext, mediaSession.test.ts Next in queue
playNext leaves the episode it left first in the queue, as play() does same
playNext queues the episode it left even with auto-queue off "with auto-queue off, plays the queue in order and queues nothing" (both files)
An arrow selects the next tab but focus stays Player.test.ts "arrow keys, Home and End move focus between the tabs"
Arrows stop at the ends instead of wrapping same
Every tab stays in the Tab order same
The arrow handler sits on the compact switch, not the wide tabs same
No reduced-motion override reducedMotion.test.ts
The override is less specific than the rule it overrides same
The override answers no-preference same
The import Notice keeps telling the user to change a setting PodNotesSettingsTab.import.test.ts "keeps the import and says it saves later"

Docs at release

  • Keyboard: in the wide full player, Left and Right move between Chapters and Up next, and Home and End go to the first and last.
  • The full player's fullscreen button hides while fullscreen. Escape leaves fullscreen.

Fullscreen on master (left) and on this branch (right), in Obsidian 1.14.4:

Fullscreen video, master left with the dead fullscreen button, this branch right without it

Feeds are always cached since #468. The constructor still accepted a
cache option that nothing read, and eight view tests still passed it.
The headset's Next in queue read the queue store and called play inline in
bindMediaSession. PlayerController now owns it, beside play and the natural
end's queue advance.
Since #470, PodNotes saves on its own once it can write data.json again.
The Notice after an import whose final save failed still told the user to
change a setting to save again.
…nel out of Player

PodcastView.svelte held 1,300 lines of page and component CSS in one global
block. Each component now carries its own styles, anchored on its own root
class with the `.root :global {}` pattern the shell already used. The shell
keeps its tokens, its layout, the Obsidian DOM rules and the classes several
components share.

Player.svelte loses the deck (title, scrubber, transport, capture bar) to
Deck.svelte, and ChaptersPanel and UpNextPanel share their chrome through
Panel.svelte. Cover draws a local file's tile, and SearchField takes the
first run's large variant, so no page reaches into them for it.

Variants of the shared .pn-iconbtn and .pn-btn classes win by specificity
instead of by stylesheet order.
The Chapters and Up next tabs follow the ARIA tabs pattern: Left and Right
move focus to the previous or next tab and wrap, Home and End go to the
first and last, and the focused tab shows its panel. Only the selected tab
is in the Tab order.
…t reduced motion

In fullscreen the video frame's fullscreen button stayed visible and did
nothing. It now hides there; Escape leaves fullscreen.

The play button's 120 ms press transition ran even when the system asks for
reduced motion. It now presses without a transition then.
screens.mjs grew past 1,100 lines. It keeps its usage header, the verb
dispatch and set; the instance calls, the seeded scenes and the contact sheet
move to screens/instance.mjs, screens/scenes.mjs and screens/sheet.mjs. The
code moved verbatim apart from imports, exports and the paths that moved one
directory down.
…to EpisodeRow

On a touch device Obsidian adds .mobile-tap to a tapped button, and its
button:not(.clickable-icon).mobile-tap rule paints a fill. Search results,
playlist rows, chapter rows and Up next's Edit button keep the specificity
that resisted it, and the mini player's open button and the rail's add
button keep the plain scoped rules that let it through.

Queue rows are an EpisodeRow variant (queue, dragging) instead of rules
QueueList laid over EpisodeRow and Cover. Deck no longer takes `wide`: the
player passes no onchapters when the chapter name cannot open a panel.
.pn-alt moves whole into AddPodcastPage, and the shell's head and back
button rules move from the shared block to its layout.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Deploying podnotes with  Cloudflare Pages  Cloudflare Pages

Latest commit: af605ca
Status: ✅  Deploy successful!
Preview URL: https://53b0007e.podnotes.pages.dev
Branch Preview URL: https://refactor-ui-structure.podnotes.pages.dev

View logs

@chhoumann
chhoumann merged commit 8853d72 into master Oct 14, 2026
19 checks passed
@chhoumann
chhoumann deleted the refactor/ui-structure branch October 14, 2026 09:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant